/*
 * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
 * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
 */

package org.jetbrains.kotlin.analysis.api.test

import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.analysis.api.KaContextParameterApi
import org.jetbrains.kotlin.analysis.api.KaCustomContextParameterBridge
import org.jetbrains.kotlin.analysis.api.KaNoContextParameterBridgeRequired
import org.jetbrains.kotlin.analysis.api.KaSession
import org.jetbrains.kotlin.analysis.api.components.KaSessionComponent
import org.jetbrains.kotlin.analysis.utils.printer.PrettyPrinter
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.test.TestDataAssertions
import org.junit.jupiter.api.Test
import java.io.File

/**
 * This test automatically generates and checks that every public API of a [KaSessionComponent]
 * has a corresponding context-parameter bridge located in the same file.
 *
 * See KT-78093 Add bridges for context parameters
 *
 * The test iterates through all [sourceDirectories] and synthesizes
 * the expected set of context-parameter bridges.
 *
 * The goal is to prevent accidental omissions or divergence of context-parameter bridges,
 * which are essential for user experience. If the absence of a bridge for a declaration is
 * intentional, the declaration must be annotated with [KaNoContextParameterBridgeRequired].
 *
 * If a custom bridge is required, it must be annotated with [KaCustomContextParameterBridge].
 *
 * How it works now:
 * 1. For each Kotlin file, the test finds a class/object that is a subtype of [KaSessionComponent]
 *    and collects all of its public callable members that are NOT annotated with
 *    [KaNoContextParameterBridgeRequired].
 * 2. Based on those members, the test GENERATES the exact text of the expected bridges
 *    (annotated with [KaContextParameterApi]) using a canonical format. Every generated bridge
 *    has a context parameter of type [KaSession] and a signature that mirrors the corresponding member.
 * 3. The test then reconstructs the file content as: original content up to the last non-bridge
 *    declaration of the component, followed by the generated bridges, and compares this text with
 *    the real file on disk. Any mismatch indicates missing, outdated, or extra bridges.
 *
 * This also effectively detects unused bridges: if a bridge exists in the source but is not
 * regenerated from a component member, the text comparison will fail.
 *
 * Assumptions:
 * 1. All public children of [KaSessionComponent] have it as their direct supertype.
 * 2. Exactly one [KaSessionComponent] is allowed per file, and it must be the first declaration.
 * 3. All context-parameter bridges are annotated with [KaContextParameterApi] and are located
 *    in the same file as the corresponding component after all regular declarations.
 */
class AnalysisApiContextParametersBridgesTest : AbstractAnalysisApiSurfaceCodebaseValidationTest() {
    @Test
    fun testContextParameterBridges() = doTest()

    override fun processFile(file: File, psiFile: PsiFile) {
        if (psiFile !is KtFile) return

        val sessionComponent = psiFile.findSessionComponent() ?: run {
            if (psiFile.declarations.any { it.hasBridgeMarker }) {
                error("There are context parameter bridges in ${psiFile.virtualFilePath} file, but no session component.")
            }

            return
        }

        val bridges = sessionComponent.generateBridges()

        val lastNonBridgeDeclaration = sessionComponent.siblings(forward = true, withItself = true)
            .filterIsInstance<KtDeclaration>()
            .takeWhile { !it.hasAutoGeneratedBridgeMarker }
            .last()

        val actualText = buildString {
            val nonBridgesPrefix = psiFile.text.take(lastNonBridgeDeclaration.endOffset)
            appendLine(nonBridgesPrefix)

            bridges.joinTo(this, prefix = "\n", separator = "\n\n")
        }

        TestDataAssertions.assertEqualsToFile(file, actualText)
    }

    private val KtDeclaration.hasBridgeMarker: Boolean
        get() = hasAutoGeneratedBridgeMarker || hasCustomBridgeMarker

    private val KtDeclaration.hasAutoGeneratedBridgeMarker: Boolean
        get() = hasAnnotation(BRIDGE_ANNOTATION_MARKER)

    private val KtDeclaration.hasCustomBridgeMarker: Boolean
        get() = hasAnnotation(CUSTOM_BRIDGE_ANNOTATION_MARKER)

    private val KtDeclaration.hasIgnoreBridgeMarker: Boolean
        get() = hasAnnotation(IGNORE_BRIDGE_ANNOTATION_MARKER)

    private fun KtClassOrObject.generateBridges(): Sequence<String> = declarations.asSequence()
        .filterIsInstance<KtCallableDeclaration>()
        .filter { it.isPublic && !it.hasIgnoreBridgeMarker }
        .map { it.generateBridgeDeclaration() }

    private fun KtCallableDeclaration.generateBridgeDeclaration(): String = PrettyPrinter(indentSize = BASE_INDENT_SIZE).apply {
        val kDocEndOffset = docComment?.textRangeInParent?.endOffset?.plus(indentSize) ?: 0

        // Add indention to the beginning of the declaration to align indent for all lines
        val callableTextWithIndentation = " ".repeat(indentSize) + text
        val callableKDoc = callableTextWithIndentation.take(kDocEndOffset)

        // Original KDoc
        if (callableKDoc.isNotBlank()) {
            appendLine(callableKDoc.trimIndent())
        }

        // Warning comment. It is placed either after the KDoc or as the first attached comment,
        // so it is a part of the declarations PSI.
        // It is not a part of the KDoc to not expose this detail to the user.
        appendLine("// Auto-generated bridge. DO NOT EDIT MANUALLY!")

        val callableTextWithoutKdoc = callableTextWithIndentation.drop(kDocEndOffset)
        val publicModifierStartOffset = modifierList?.visibilityModifier()!!.getStartOffsetIn(this@generateBridgeDeclaration)
        val declarationAnnotations = callableTextWithoutKdoc.take(publicModifierStartOffset - kDocEndOffset)

        // Original annotations
        if (declarationAnnotations.isNotBlank()) {
            appendLine(declarationAnnotations.trimIndent())
        }

        // New marker annotation
        append('@')
        appendLine(BRIDGE_ANNOTATION_MARKER)

        // New context parameter
        append("context(")
        // One letter name is chosen intentionally to avoid conflicts with any potential regular parameters
        // since they are expected to be meaningful
        val contextParameterName = "s"
        append(contextParameterName)
        append(": ")
        append(KA_SESSION_CLASS)
        appendLine(')')

        val signatureEndOffset = typeConstraintList?.textRangeInParent?.endOffset ?: typeReference!!.textRangeInParent.endOffset
        val signature = callableTextWithIndentation.substring(publicModifierStartOffset, signatureEndOffset + indentSize)

        // Original signature without body
        append(signature.trimIndent())

        // Original declaration is deprecated -> suppression on the call site is required for compilation
        val suppressDeprecationStatement = if (hasAnnotation(DEPRECATED_ANNOTATION)) {
            """@Suppress("DEPRECATION")"""
        } else {
            null
        }

        when (this@generateBridgeDeclaration) {
            is KtProperty -> {
                appendLine()
                withIndent {
                    suppressDeprecationStatement?.let(::appendLine)
                    append("get() = with($contextParameterName) { $name }")
                }
            }

            is KtNamedFunction -> {
                appendLine(" {")
                withIndent {
                    suppressDeprecationStatement?.let(::appendLine)
                    appendLine("return with($contextParameterName) {")
                    withIndent {
                        append(name)
                        append('(')
                        withIndent {
                            printCollectionIfNotEmpty(valueParameters, separator = "", prefix = "\n") { parameter ->
                                val name = parameter.name
                                append(name)
                                append(" = ")
                                append(name)
                                append(",\n")
                            }
                        }

                        appendLine(')')
                    }
                    appendLine("}")
                }
                append('}')
            }
        }
    }.toString()

    private companion object {
        private val BRIDGE_ANNOTATION_MARKER: String = KaContextParameterApi::class.simpleName!!
        private val CUSTOM_BRIDGE_ANNOTATION_MARKER: String = KaCustomContextParameterBridge::class.simpleName!!
        private val IGNORE_BRIDGE_ANNOTATION_MARKER: String = KaNoContextParameterBridgeRequired::class.simpleName!!
        private val DEPRECATED_ANNOTATION: String = Deprecated::class.simpleName!!
        private const val BASE_INDENT_SIZE = 4
    }
}